Xiuno BBS 机制精讲 - 附件
# Xiuno BBS 帖子附件上传、下载、删除功能详解
本文档由TRAE编写。本文档使用了AI辅助生成,可能包含错误或不完整的内容,需要校对。
## 一、概述
Xiuno BBS的附件系统是一个完整的文件管理子系统,支持用户在发帖和回帖时上传各类文件附件。该系统具有以下特点:
- **两阶段上传机制**:附件先上传到临时目录,帖子发布后才转为正式附件
- **Session临时存储**:未发布的附件信息存储在Session中,避免数据库污染
- **权限控制**:上传、下载、删除都有完善的权限验证
- **分类管理**:自动识别文件类型,分为图片和非图片两大类
## 二、核心数据结构
### 2.1 附件数据表结构
附件信息存储在 `bbs_attach` 表中,主要字段包括:
| 字段名 | 类型 | 说明 |
|--------|------|------|
| aid | int | 附件ID(主键) |
| tid | int | 所属主题ID |
| pid | int | 所属帖子ID |
| uid | int | 上传者用户ID |
| filesize | int | 文件大小(字节) |
| width | int | 图片宽度(仅图片) |
| height | int | 图片高度(仅图片) |
| filename | varchar | 存储文件名(含日期目录) |
| orgfilename | varchar | 原始文件名 |
| filetype | varchar | 文件类型分类 |
| create_date | int | 创建时间戳 |
| downloads | int | 下载次数 |
| isimage | tinyint | 是否为图片(1=是,0=否) |
### 2.2 Session中的临时附件结构
上传但未发布的附件存储在 `$_SESSION['tmp_files']` 数组中:
```php
$_SESSION['tmp_files'] = array(
0 => array(
'url' => 'upload/tmp/1_abc123.jpg', // 临时URL
'path' => './upload/tmp/1_abc123.jpg', // 临时物理路径
'orgfilename' => '原始文件名.jpg', // 原始文件名
'filetype' => 'image', // 文件类型
'filesize' => 102400, // 文件大小
'width' => 800, // 图片宽度
'height' => 600, // 图片高度
'isimage' => 1, // 是否为图片
'downloads' => 0, // 下载次数
'aid' => '_0' // 附件ID(下划线开头则为临时附件)
),
// ... 更多附件
);
```
## 三、附件上传流程详解
### 3.1 上传入口
上传入口URL:`attach-create`
对应的路由文件:route/attach.php
### 3.2 上传流程步骤
#### 步骤1:权限验证
```php
$user = user_read($uid);
user_login_check();
// 检查用户组是否有上传权限
empty($group['allowattach']) AND $gid != 1 AND message(-1, '您无权上传');
```
#### 步骤2:接收上传数据
Xiuno BBS采用Base64编码传输文件数据:
```php
$data = param_base64('data'); // 获取Base64编码的文件数据
$size = strlen($data); // 计算文件大小
// 文件大小限制(最大20MB)
$size > 20480000 AND message(-1, lang('filesize_too_large'));
```
**重要提醒**:该方式并不高效!
#### 步骤3:生成临时文件名
```php
$ext = file_ext($name, 7); // 获取文件扩展名
$tmpanme = $uid.'_'.xn_rand(15).'.'.$ext; // 生成:用户ID_随机字符.扩展名
$tmpfile = $conf['upload_path'].'tmp/'.$tmpanme; // 临时文件路径
$tmpurl = $conf['upload_url'].'tmp/'.$tmpanme; // 临时URL
```
#### 步骤4:保存临时文件
```php
file_put_contents($tmpfile, $data) OR message(-1, lang('write_to_file_failed'));
```
**重要提醒**:并没有对上传的文件进行内容核实,需要手动在附近的hook点出增加代码来确定上传的文件确实如文件扩展名所述,例如,可以上传一个.jpg格式的PHP Webshell文件,即使有随机文件名进行混淆,但风险依旧存在。
#### 步骤5:存储到Session
```php
// 重新启动session,避免并发写入问题
sess_restart();
// 初始化session数组
empty($_SESSION['tmp_files']) AND $_SESSION['tmp_files'] = array();
// 计算附件索引
$n = count($_SESSION['tmp_files']);
// 构建附件信息
$attach = array(
'url' => $tmpurl,
'path' => $tmpfile,
'orgfilename' => $name,
'filetype' => $filetype,
'filesize' => filesize($tmpfile),
'width' => $width,
'height' => $height,
'isimage' => $is_image,
'downloads' => 0,
'aid' => '_'.$n // 临时ID,下划线开头
);
$_SESSION['tmp_files'][$n] = $attach;
```
### 3.3 临时附件ID的特殊含义
临时附件的 `aid` 以下划线开头(如 `_0`, `_1`),这是Xiuno BBS的一个重要设计:
- **临时标识**:下划线开头表示附件尚未关联到帖子
- **索引映射**:数字部分对应 `$_SESSION['tmp_files']` 数组的索引
- **状态区分**:便于在删除时区分临时附件和正式附件
## 四、附件关联流程(临时附件转正式附件)
### 4.1 触发时机
附件关联发生在以下情况:
- 发布新主题(`thread_create`)
- 发布回帖(`post_create`)
- 编辑帖子(`post_update`)
### 4.2 关联函数:attach_assoc_post()
该函数定义在 model/attach.func.php 中。
#### 关联流程详解:
```php
function attach_assoc_post($pid) {
global $uid, $time, $conf;
// 1. 从Session获取临时文件列表
$sess_tmp_files = _SESSION('tmp_files');
// 2. 读取帖子信息
$post = post__read($pid);
if(empty($post)) return;
$tid = $post['tid'];
// 3. 处理每个临时文件
if($tmp_files) {
foreach($tmp_files as $file) {
// 3.1 计算目标目录(按日期分目录)
$day = date($attach_dir_save_rule, $time); // 如 "202603"
$path = $conf['upload_path'].'attach/'.$day;
$url = $conf['upload_url'].'attach/'.$day;
// 3.2 创建目录
!is_dir($path) AND mkdir($path, 0777, TRUE);
// 3.3 移动文件
$destfile = $path.'/'.$filename;
xn_copy($file['path'], $destfile);
// 3.4 删除临时文件
if(is_file($destfile) && filesize($destfile) == filesize($file['path'])) {
@unlink($file['path']);
}
// 3.5 创建数据库记录
$arr = array(
'tid' => $tid,
'pid' => $pid,
'uid' => $uid,
'filesize' => $file['filesize'],
'width' => $file['width'],
'height' => $file['height'],
'filename' => "$day/$filename",
'orgfilename' => $file['orgfilename'],
'filetype' => $file['filetype'],
'create_date' => $time,
'downloads' => 0,
'isimage' => $file['isimage']
);
$aid = attach_create($arr);
// 3.6 更新帖子内容中的URL
$post['message'] = str_replace($file['url'], $desturl, $post['message']);
}
}
// 4. 清空Session中的临时文件
$_SESSION['tmp_files'] = array();
// 5. 更新帖子的图片和文件计数
list($attachlist, $imagelist, $filelist) = attach_find_by_pid($pid);
$images = count($imagelist);
$files = count($filelist);
post__update($pid, array('images'=>$images, 'files'=>$files));
}
```
### 4.3 目录存储规则
附件按日期分目录存储,由配置项 `attach_dir_save_rule` 控制:
| 配置值 | 目录示例 | 适用场景 |
|--------|----------|----------|
| `Ym` | `upload/attach/202603/` | 【默认值】附件较少,按月存储 |
| `Ymd` | `upload/attach/20260329/` | 附件较多,按日存储 |
## 五、附件下载流程详解
### 5.1 下载入口
下载入口URL:`attach-download-{aid}`
### 5.2 下载流程步骤
#### 步骤1:读取附件信息
```php
$aid = param(2, 0);
$attach = attach_read($aid);
empty($attach) AND message(-1, lang('attach_not_exists'));
```
#### 步骤2:权限验证
```php
$tid = $attach['tid'];
$thread = thread_read($tid);
$fid = $thread['fid'];
// 检查用户是否有下载权限
$allowdown = forum_access_user($fid, $gid, 'allowdown');
empty($allowdown) AND message(-1, lang('insufficient_privilege_to_download'));
```
#### 步骤3:更新下载计数
```php
attach_update($aid, array('downloads+'=>1));
```
#### 步骤4:输出文件
Xiuno BBS支持两种输出方式:
**方式一:PHP直接输出(默认)**
```php
// 设置响应头
header('Content-Disposition: attachment; filename="'.$attach['orgfilename'].'"');
header('Content-Type: application/octet-stream');
// 输出文件内容
readfile($attachpath);
exit;
```
**方式二:重定向到文件URL**
该方法是为后续可能的扩展而提供的,目前未被使用。
```php
http_location($attachurl);
```
### 5.3 IE/Edge浏览器兼容处理
```php
if(stripos($_SERVER["HTTP_USER_AGENT"], 'MSIE') !== FALSE ||
stripos($_SERVER["HTTP_USER_AGENT"], 'Edge') !== FALSE ||
stripos($_SERVER["HTTP_USER_AGENT"], 'Trident') !== FALSE) {
$attach['orgfilename'] = urlencode($attach['orgfilename']);
$attach['orgfilename'] = str_replace("+", "%20", $attach['orgfilename']);
}
```
## 六、附件删除流程详解
### 6.1 删除入口
删除入口URL:`attach-delete-{aid}`
### 6.2 两种删除场景
#### 场景一:删除临时附件
当 `aid` 以下划线开头时,表示删除临时附件:
```php
if(substr($aid, 0, 1) == '_') {
$key = intval(substr($aid, 1)); // 获取索引
$tmp_files = _SESSION('tmp_files');
// 验证附件存在
!isset($tmp_files[$key]) AND message(-1, lang('item_not_exists'));
$attach = $tmp_files[$key];
// 删除物理文件
!is_file($attach['path']) AND message(-1, lang('file_not_exists'));
unlink($attach['path']);
// 从Session中移除
unset($_SESSION['tmp_files'][$key]);
}
```
#### 场景二:删除正式附件
```php
else {
$aid = intval($aid);
$attach = attach_read($aid);
empty($attach) AND message(-1, lang('attach_not_exists'));
// 权限验证
$thread = thread_read($attach['tid']);
$fid = $thread['fid'];
$allowdelete = forum_access_mod($fid, $gid, 'allowdelete');
$attach['uid'] != $uid AND !$allowdelete AND message(0, lang('insufficient_privilege'));
// 执行删除
$r = attach_delete($aid);
}
```
### 6.3 attach_delete() 函数
```php
function attach_delete($aid) {
global $conf;
// 1. 读取附件信息
$attach = attach_read($aid);
// 2. 删除物理文件
$path = $conf['upload_path'].'attach/'.$attach['filename'];
file_exists($path) AND unlink($path);
// 3. 删除数据库记录
$r = attach__delete($aid);
return $r;
}
```
## 七、附件类型分类
### 7.1 支持的文件类型
定义在 [conf/attach.conf.php](file:///m:/laragon/www/xiunobbs-v4.0.7/conf/attach.conf.php):
| 类型 | 扩展名 |
|------|--------|
| image | gif, jpg, jpeg, png, bmp, webp |
| video | av, wmv, wav, wma, avi, mp4 |
| music | mp3, mp4 |
| office | doc, xls, ppt, docx, xlsx, pptx |
| pdf | pdf |
| text | c, cpp, cc, txt |
| zip | tar, zip, gz, rar, 7z, bz |
| flash | swf, fla, as |
| exe | exe, bin |
| book | chm |
| torrent | bt, torrent |
| font | ttf, font, fon |
应根据实际需求添加或删除文件类型,例如video类缺少webm、music类缺少ogg等。
### 7.2 类型判断函数
```php
function attach_type($name, $types) {
$ext = file_ext($name);
foreach($types as $type=>$exts) {
if($type == 'all') continue;
if(in_array($ext, $exts)) {
return $type;
}
}
return 'other';
}
```
### 7.3 图片附件的特殊处理
图片附件(`isimage = 1`)有以下特殊之处:
1. **所见即所得编辑器集成**:上传后会返回Json,用于直接插入编辑器
2. **尺寸记录**:保存 `width` 和 `height` 信息
3. **内容嵌入**:图片URL直接嵌入帖子内容中
4. **前端展示**:在帖子列表和详情页直接显示
## 八、垃圾回收机制
### 8.1 临时文件清理
```php
function attach_gc() {
global $time, $conf;
$tmpfiles = glob($conf['upload_path'].'tmp/*.*');
if(is_array($tmpfiles)) {
foreach($tmpfiles as $file) {
// 清理超过24小时的临时文件
if($time - filemtime($file) > 86400) {
unlink($file);
}
}
}
}
```
### 8.2 触发时机
垃圾回收通常在每日定时任务中执行,清理以下内容:
- 超过24小时未关联的临时文件
- Session中过期的临时附件记录
## 九、安全机制
### 9.1 文件类型白名单
```php
$filetypes = include APP_PATH.'conf/attach.conf.php';
!in_array($ext, $filetypes['all']) AND $ext = '_'.$ext; // 不在白名单则添加下划线前缀
```
**重要提醒**:不要依赖这个白名单,因为用户可以上传任意文件,还是那句话,xiuno bbs没有对文件内容做检查。
### 9.2 文件大小限制
- 默认最大:20MB(20480000字节)
- 可通过修改代码调整
### 9.3 权限控制
| 操作 | 权限检查 |
|------|----------|
| 上传 | `allowattach` 用户组权限 |
| 下载 | `allowdown` 版块权限 |
| 删除 | 所有者或版主权限 |
### 9.4 Session并发处理
```php
// 抛弃之前的 $_SESSION 数据,重新启动 session
// 降低 session 并发写入的问题
sess_restart();
```
## 十、与帖子系统的关联
### 10.1 发帖时的附件处理
```
用户上传附件 → 存储到临时目录 → 存入Session
↓
用户提交帖子 → thread_create() / post_create()
↓
attach_assoc_post() → 移动文件 → 创建数据库记录 → 更新帖子内容
```
### 10.2 编辑帖子时的附件处理
```
用户编辑帖子 → 加载已有附件列表
↓
用户上传新附件 → 存储到临时目录 → 存入Session
↓
用户提交更新 → post_update()
↓
attach_assoc_post() → 处理新附件
```
### 10.3 删除帖子时的附件处理
```php
function post_delete($pid) {
// ...
// 删除帖子关联的所有附件
($post['images'] || $post['files']) AND attach_delete_by_pid($pid);
// ...
}
function attach_delete_by_pid($pid) {
list($attachlist, $imagelist, $filelist) = attach_find_by_pid($pid);
foreach($attachlist as $attach) {
// 删除物理文件
$path = $conf['upload_path'].'attach/'.$attach['filename'];
file_exists($path) AND unlink($path);
// 删除数据库记录
attach__delete($attach['aid']);
}
return count($attachlist);
}
```
## 十一、前端交互
### 11.1 上传请求格式
```javascript
$.xpost(url('attach-create'), {
name: '文件名.jpg',
data: 'base64编码的文件数据',
width: 800, // 图片宽度
height: 600, // 图片高度
is_image: 1 // 是否为图片
}, function(code, message) {
// message 包含附件信息,包括临时 aid
});
```
### 11.2 返回数据格式
```json
{
"code": 0,
"message": {
"url": "upload/tmp/1_abc123.jpg",
"orgfilename": "原始文件名.jpg",
"filetype": "image",
"filesize": 102400,
"width": 800,
"height": 600,
"isimage": 1,
"downloads": 0,
"aid": "_0"
}
}
```